if (mNIC.hWnd) // i.e. only update the tip if the tray icon exists (can't work otherwise).
{
UPDATE_TIP_FIELD
Shell_NotifyIcon(NIM_MODIFY, &mNIC); // Currently not checking its result (e.g. in case a shell other than Explorer is running).
}
return OK;
case MENU_CMD_ICON:
{
RETURN_IF_NOT_TRAY;
bool mIconFrozen_prev = mIconFrozen;
if (*aOptions) // i.e. if it's blank, don't change the current setting of mIconFrozen.
mIconFrozen = (ATOI(aOptions) == 1);
if (!*aParam3)
{
g_NoTrayIcon = false;
if (!mNIC.hWnd) // The icon doesn't exist, so create it.
{
CreateTrayIcon();
UpdateTrayIcon(true); // Force the icon into the correct pause/suspend state.
}
else if (!mIconFrozen && mIconFrozen_prev) // To cause "Menu Tray, Icon,,, 0" to update the icon while the script is suspended.
UpdateTrayIcon(true);
return OK;
}
// Otherwise, user has specified a custom icon:
if (*aParam3 == '*' && !*(aParam3 + 1)) // Restore the standard icon.
{
if (mCustomIcon)
{
GuiType::DestroyIconIfUnused(mCustomIcon); // v1.0.37.07: Solves reports of Gui windows losing their icons.
// If the above doesn't destroy the icon, the GUI window(s) still using it are responsible for
// destroying it later.
mCustomIcon = NULL; // To indicate that there is no custom icon.
if (mCustomIconFile)
*mCustomIconFile = '\0';
mCustomIconNumber = 0;
UpdateTrayIcon(true); // Need to use true in this case too.
}
return OK;
}
// v1.0.43.03: Load via LoadPicture() vs. ExtractIcon() because ExtractIcon harms the quality
// of 16x16 icons inside .ico files by first scaling them to 32x32 (which then has to be scaled
// back to 16x16 for the tray and for the SysMenu icon). I've visually confirmed that the
// distortion occurs at least when a 16x16 icon is loaded by ExtractIcon() then put into the
// tray. It might not be the scaling itself that distorts the icon: the pixels are all in the
// right places, it's just that some are the wrong color/shade. This implies that some kind of
// unwanted interpolation or color tweaking is being done by ExtractIcon (and probably LoadIcon),
// but not by LoadImage.
// Also, load the icon at actual size so that when/if this icon is used for a GUI window, its
// appearance in the alt-tab menu won't be unexpectedly poor due to having been scaled from its
// native size down to 16x16.
int icon_number;
if (*aParam4)
{
icon_number = ATOI(aParam4);
if (icon_number < 1) // Must validate for use in two places below.
icon_number = 1; // Must be >0 to tell LoadPicture that "icon must be loaded, never a bitmap".
}
else
icon_number = 1; // One vs. Zero tells LoadIcon: "must load icon, never a bitmap (e.g. no gif/jpg/png)".
int image_type;
HICON new_icon;
if ( !(new_icon = (HICON)LoadPicture(aParam3, 0, 0, image_type, icon_number, false)) ) // Called with icon_number > 0, it guarantees return of an HICON/HCURSOR, never an HBITMAP.
RETURN_MENU_ERROR("Can't load icon.", aParam3);
GuiType::DestroyIconIfUnused(mCustomIcon); // This destroys it if non-NULL and it's not used by an GUI windows.
mCustomIcon = new_icon;
mCustomIconNumber = icon_number;
// Allocate the full MAX_PATH in case the contents grow longer later.
// SimpleHeap improves avg. case mem load:
if (!mCustomIconFile)
mCustomIconFile = SimpleHeap::Malloc(MAX_PATH);
if (mCustomIconFile)
{
// Get the full path in case it's a relative path. This is documented and it's done in case
// the script ever changes its working directory:
char full_path[MAX_PATH], *filename_marker;
if (GetFullPathName(aParam3, sizeof(full_path) - 1, full_path, &filename_marker))
strlcpy(mCustomIconFile, full_path, MAX_PATH);
else
strlcpy(mCustomIconFile, aParam3, MAX_PATH);
}
if (!g_NoTrayIcon)
UpdateTrayIcon(true); // Need to use true in this case too.
return OK;
}
case MENU_CMD_NOICON:
RETURN_IF_NOT_TRAY;
g_NoTrayIcon = true;
if (mNIC.hWnd) // Since it exists, destroy it.
{
Shell_NotifyIcon(NIM_DELETE, &mNIC); // Remove it.
mNIC.hWnd = NULL; // Set this as an indicator that tray icon is not installed.
// but don't do DestroyMenu() on mTrayMenu->mMenu (if non-NULL) since it may have been
// changed by the user to have the custom items on top of the standard items,
// for example, and we don't want to lose that ordering in case the script turns
// the icon back on at some future time during this session.
}
return OK;
case MENU_CMD_CLICK:
RETURN_IF_NOT_TRAY;
mTrayMenu->mClickCount = ATOI(aParam3);
if (mTrayMenu->mClickCount < 1)
mTrayMenu->mClickCount = 1; // Single-click to activate menu's default item.
else if (mTrayMenu->mClickCount > 2)
mTrayMenu->mClickCount = 2; // Double-click.
return OK;
case MENU_CMD_MAINWINDOW:
RETURN_IF_NOT_TRAY;
#ifdef AUTOHOTKEYSC
if (!g_AllowMainWindow)
{
g_AllowMainWindow = true;
// Rather than using InsertMenu() to insert the item in the right position,
// which makes the code rather unmaintainable, it seems best just to recreate
// the entire menu. This will result in the standard menu items going back
// up to the top of the menu if the user previously had them at the bottom,
// but it seems too rare to worry about, especially since it's easy to
// work around that:
if (mTrayMenu->mIncludeStandardItems)
mTrayMenu->Destroy(); // It will be recreated automatically the next time the user displays it.
// else there's no need.
}
#endif
return OK;
case MENU_CMD_NOMAINWINDOW:
RETURN_IF_NOT_TRAY;
#ifdef AUTOHOTKEYSC
if (g_AllowMainWindow)
{
g_AllowMainWindow = false;
// See comments in the prior case above for why it's done this way vs. using DeleteMenu():
if (mTrayMenu->mIncludeStandardItems)
mTrayMenu->Destroy(); // It will be recreated automatically the next time the user displays it.
// else there's no need.
}
#endif
return OK;
} // switch()
// Now that most opportunities to return an error have passed, find or create the menu, since
// all the commands that haven't already been fully handled above will need it:
UserMenu *menu = FindMenu(aMenu);
if (!menu)
{
// Menus can be created only in conjuction with the ADD command. Update: As of v1.0.25.12, they can
// also be created with the "Menu, MyMenu, Standard" command.
if (menu_command != MENU_CMD_ADD && menu_command != MENU_CMD_STANDARD)
RETURN_MENU_ERROR(ERR_MENU, aMenu);
if ( !(menu = AddMenu(aMenu)) )
RETURN_MENU_ERROR("Menu name too long.", aMenu); // Could also be "out of mem" but that's too rare to display.
}
// The above has found or added the menu for use below.
UPDATE_GUI_MENU_BARS(mMenuType, mMenu) // Verified as being necessary (though it would be rare anyone would want the menu bar containing the std items).
return OK; // For caller convenience.
}
ResultType UserMenu::Destroy()
// Returns OK upon complete success or FAIL otherwise. For example, even if this's menu
// is successfully destroyed, if the indirect destructions resulting from it don't succeed, this
// method returns FAIL.
{
if (!mMenu) // For performance.
return OK;
// I think DestroyMenu() can fail if an attempt is made to destroy the menu while it is being
// displayed (but even if it doesn't fail, it seems very bad to try to destroy it then, which
// is why g_MenuIsVisible is checked just to be sure).
// But this all should be impossible in our case because the script is in an uninterruptible state
// while the menu is displayed, which in addition to pausing the current thread (which happens
// anyway), no new timed or hotkey subroutines can be launched. Thus, this should rarely if
// ever happen, which is why no error message is given here:
//if (g_MenuIsVisible)
// return FAIL;
// DestroyMenu fails (GetLastError() == ERROR_INVALID_MENU_HANDLE) if a parent menu that contained
// mMenu as one of its submenus was destroyed above. This seems to indicate that submenus are
// destroyed whenever a parent menu is destroyed. Therefore, don't check failure on the below,
// just assume that afterward, the menu is gone. IsMenu() is checked because the handle can be
// invalid if the OS already destroyed it behind-the-scenes (this happens to a submenu whenever
// its parent menu is destroyed, or whenever a submenu is converted back into a normal menu item):
if (IsMenu(mMenu))
{
// As a precaution, don't allow a menu to be destroyed if a window is using it as its
// menu bar. That might have bad side-effects on some OSes, especially older ones:
if (mMenuType == MENU_TYPE_BAR && GuiType::sGuiCount)
{
int i, gui_count;
for (i = 0, gui_count = 0; i < MAX_GUI_WINDOWS; ++i)
if (g_gui[i])
{
if (g_gui[i]->mHwnd && GetMenu(g_gui[i]->mHwnd) == mMenu)
return FAIL; // A GUI window is using this menu, so don't destroy the menu.
if (GuiType::sGuiCount == ++gui_count) // No need to keep searching.
break;
}
}
if (!DestroyMenu(mMenu)) // v1.0.30.01: Doesn't seem to be a reason *not* to check the return value and return FAIL if it failed.
return FAIL;
}
mMenu = NULL; // This must be done immediately after destroying the menu to prevent recursion problems below.
// Bug-fix for v1.0.19: The below is now done OUTSIDE the above block because the moment a
// parent menu is deleted all its submenus AND SUB-SUB-SUB...MENUS become invalid menu handles.
// But even though the OS has done this, Destroy() must still be called recursively from here
// so that the menu handles will be set to NULL. This is because other functions -- such as
// Display() -- do not do the IsMenu() check, relying instead on whether the handle is NULL to
// determine whether the menu physically exists.
// The moment the above is done, any submenus that were attached to mMenu are also destroyed
// by the OS. So mark them as destroyed in our bookkeeping also:
UserMenuItem *mi;
for (mi = mFirstMenuItem; mi ; mi = mi->mNextMenuItem)
if (mi->mSubmenu && mi->mSubmenu->mMenu && !IsMenu(mi->mSubmenu->mMenu))
mi->mSubmenu->Destroy(); // Its return value isn't checked since there doesn't seem to be anything that can/should be done if it fails.
// Destroy any menu that contains this menu as a submenu. This is done so that such
// menus will be automatically recreated the next time they are used, which is necessary
// because otherwise when such a menu is displayed the next time, the OS will show its
// old contents even though the menu is gone. Thus, those old menu items will be
// selectable but will have no effect. In addition, sometimes our caller plans to
// recreate this->mMenu (or have it recreated automatically upon first use) and thus
// we don't want to use DeleteMenu() because that would require having to detect whether
// the menu needs updating (to reflect whether the submenu has been recreated) every
// time we display it. Another drawback to DeleteMenu() is that it would change the
// order of the menu items to something other than what the user originally specified
// unless InsertMenu() was woven in during the update:
ResultType result = OK;
for (UserMenu *m = g_script.mFirstMenu; m; m = m->mNextMenu)
if (m->mMenu)
for (mi = m->mFirstMenuItem; mi; mi = mi->mNextMenuItem)
if (mi->mSubmenu == this)
if (!m->Destroy()) // Attempt to destroy any menu that contains this menu as a submenu (will fail if m is a menu bar).
result = FAIL; // Seems best to consider even one failure is considered a total failure.
return result;
}
ResultType UserMenu::Display(bool aForceToForeground, int aX, int aY)
// aForceToForeground defaults to true because when a menu is displayed spontanesouly rather than
// in response to the user right-clicking the tray icon, I believe that the OS will revert to its
// behavior of "resisting" a window that tries to "steal focus". I believe this resistance does
// not occur when the user clicks the icon because that click causes the task bar to get focus,
// and it is likely that the OS allows other windows to steal focus from the task bar without
// resistence. This is done because if the main window is *not* successfully activated prior to
// displaying the menu, it might be impossible to dismiss the menu by clicking outside of it.
{
if (!mMenuItemCount && !mIncludeStandardItems)
return OK; // Consider the display of an empty menu to be a success.
//if (!IsMenu(mMenu))
// mMenu = NULL;
if (!mMenu) // i.e. because this is the first time the user has opened the menu.
if (!Create()) // no error msg since so rare
return FAIL;
if (this == g_script.mTrayMenu)
{
// These are okay even if the menu items don't exist (perhaps because the user customized the menu):
if (aX == COORD_UNSPECIFIED || aY == COORD_UNSPECIFIED)
GetCursorPos(&pt);
if (!(aX == COORD_UNSPECIFIED && aY == COORD_UNSPECIFIED)) // At least one was specified.
{
if (aX != COORD_UNSPECIFIED)
pt.x = aX;
if (aY != COORD_UNSPECIFIED)
pt.y = aY;
if (!(g.CoordMode & COORD_MODE_MENU)) // Using coords relative to the active window (rather than screen).
WindowToScreen((int &)pt.x, (int &)pt.y);
}
// UPDATE: For v1.0.35.14, must ensure one of the script's windows is active before showing the menu
// because otherwise the menu cannot be dismissed via the escape key or by clicking outside the menu.
// Testing shows that ensuring any of our thread's windows is active allows both the tray menu and
// any popup or context menus to work correctly.
// UPDATE: For v1.0.35.12, the script's main window (g_hWnd) is activated only for the tray menu because:
// 1) Doing so for GUI context menus seems to prevent mouse clicks in the menu or elsewhere in the window.
// 2) It would probably have other side effects for other uses of popup menus.
HWND fore_win = GetForegroundWindow();
bool change_fore;
if (change_fore = (!fore_win || GetWindowThreadProcessId(fore_win, NULL) != g_MainThreadID))
{
// Always bring main window to foreground right before TrackPopupMenu(), even if window is hidden.
// UPDATE: This is a problem because SetForegroundWindowEx() will restore the window if it's hidden,
// but restoring also shows the window if it's hidden. Could re-hide it... but the question here
// is can a minimized window be the foreground window? If not, how to explain why
// SetForegroundWindow() always seems to work for the purpose of the tray menu?
//if (aForceToForeground)
//{
// // Seems best to avoid using the script's current setting of #WinActivateForce. Instead, always
// // try the gentle approach first since it is unlikely that displaying a menu will cause the
// // "flashing task bar button" problem?
// bool original_setting = g_WinActivateForce;
// g_WinActivateForce = false;
// SetForegroundWindowEx(g_hWnd);
// g_WinActivateForce = original_setting;
//}
//else
if (!SetForegroundWindow(g_hWnd))
{
// The below fixes the problem where the menu cannot be canceled by clicking outside of
// it (due to the main window not being active). That usually happens the first time the
// menu is displayed after the script launches. 0 is not enough sleep time, but 10 is:
SLEEP_WITHOUT_INTERRUPTION(10);
SetForegroundWindow(g_hWnd); // 2nd time always seems to work for this particular window.
// OLDER NOTES:
// Always bring main window to foreground right before TrackPopupMenu(), even if window is hidden.
// UPDATE: This is a problem because SetForegroundWindowEx() will restore the window if it's hidden,
// but restoring also shows the window if it's hidden. Could re-hide it... but the question here
// is can a minimized window be the foreground window? If not, how to explain why
// SetForegroundWindow() always seems to work for the purpose of displaying the tray menu?
//if (aForceToForeground)
//{
// // Seems best to avoid using the script's current setting of #WinActivateForce. Instead, always
// // try the gentle approach first since it is unlikely that displaying a menu will cause the
// // "flashing task bar button" problem?
// bool original_setting = g_WinActivateForce;
// g_WinActivateForce = false;
// SetForegroundWindowEx(g_hWnd);
// g_WinActivateForce = original_setting;
//}
//else
//...
}
}
// Apparently, the HWND parameter of TrackPopupMenuEx() can be g_hWnd even if one of the script's
// other (non-main) windows is foreground. The menu still seems to operate correctly.
g_MenuIsVisible = MENU_TYPE_POPUP; // It seems this is also set by HANDLE_MENU_LOOP because apparently, TrackPopupMenuEx generates WM_ENTERMENULOOP. So it's done here just for added safety in case WM_ENTERMENULOOP isn't ALWAYS generated.